/**********************************************************************

    Intel 8255(A) Programmable Peripheral Interface emulation

    Copyright MESS Team.
    Visit http://mamedev.org for licensing and usage restrictions.

**********************************************************************/

#include "i8255.h"
#include "machine/devhelpr.h"



//**************************************************************************
//  MACROS / CONSTANTS
//**************************************************************************

#define LOG 0


enum
{
	PORT_A = 0,
	PORT_B,
	PORT_C,
	CONTROL
};


enum
{
	GROUP_A = 0,
	GROUP_B
};


enum
{
	MODE_0 = 0,
	MODE_1,
	MODE_2
};


enum
{
	MODE_OUTPUT = 0,
	MODE_INPUT,
};


#define CONTROL_PORT_C_LOWER_INPUT	0x01
#define CONTROL_PORT_B_INPUT		0x02
#define CONTROL_GROUP_B_MODE_1		0x04
#define CONTROL_PORT_C_UPPER_INPUT	0x08
#define CONTROL_PORT_A_INPUT		0x10
#define CONTROL_GROUP_A_MODE_MASK	0x60
#define CONTROL_MODE_SET			0x80



//**************************************************************************
//  DEVICE DEFINITIONS
//**************************************************************************

const device_type I8255 = &device_creator<i8255_device>;
const device_type I8255A = &device_creator<i8255_device>;



//**************************************************************************
//  INLINE HELPERS
//**************************************************************************

//-------------------------------------------------
//  check_interrupt -
//-------------------------------------------------

inline void i8255_device::check_interrupt(int port)
{
	switch (group_mode(port))
	{
	case MODE_1:
		switch (port_mode(port))
		{
		case MODE_INPUT:
			set_intr(port, (m_inte[port] && m_ibf[port]));
			break;

		case MODE_OUTPUT:
			set_intr(port, (m_inte[port] && m_obf[port]));
			break;
		}
		break;

	case MODE_2:
		set_intr(port, ((m_inte1 && m_obf[port]) || (m_inte2 && m_ibf[port])));
		break;
	}
}


//-------------------------------------------------
//  set_ibf -
//-------------------------------------------------

inline void i8255_device::set_ibf(int port, int state)
{
	if (LOG) logerror("I8255 '%s' Port %c IBF: %u\n", tag(), 'A' + port, state);

	m_ibf[port] = state;

	check_interrupt(port);
}


//-------------------------------------------------
//  set_obf -
//-------------------------------------------------

inline void i8255_device::set_obf(int port, int state)
{
	if (LOG) logerror("I8255 '%s' Port %c OBF: %u\n", tag(), 'A' + port, state);

	m_obf[port] = state;

	check_interrupt(port);
}


//-------------------------------------------------
//  set_inte -
//-------------------------------------------------

inline void i8255_device::set_inte(int port, int state)
{
	if (LOG) logerror("I8255 '%s' Port %c INTE: %u\n", tag(), 'A' + port, state);

	m_inte[port] = state;

	check_interrupt(port);
}


//-------------------------------------------------
//  set_inte1 -
//-------------------------------------------------

inline void i8255_device::set_inte1(int state)
{
	if (LOG) logerror("I8255 '%s' Port A INTE1: %u\n", tag(), state);

	m_inte1 = state;

	check_interrupt(PORT_A);
}


//-------------------------------------------------
//  set_inte2 -
//-------------------------------------------------

inline void i8255_device::set_inte2(int state)
{
	if (LOG) logerror("I8255 '%s' Port A INTE2: %u\n", tag(), state);

	m_inte2 = state;

	check_interrupt(PORT_A);
}


//-------------------------------------------------
//  set_intr -
//-------------------------------------------------

inline void i8255_device::set_intr(int port, int state)
{
	if (LOG) logerror("I8255 '%s' Port %c INTR: %u\n", tag(), 'A' + port, state);

	m_intr[port] = state;

	output_pc();
}


//-------------------------------------------------
//  group_mode -
//-------------------------------------------------

inline int i8255_device::group_mode(int group)
{
	int mode = 0;

	switch (group)
	{
	case GROUP_A:
		switch ((m_control & CONTROL_GROUP_A_MODE_MASK) >> 5)
		{
		case 0: mode = MODE_0; break;
		case 1: mode = MODE_1; break;
		case 2: case 3: mode = MODE_2; break;
		}
		break;

	case GROUP_B:
		mode = m_control & CONTROL_GROUP_B_MODE_1 ? MODE_1 : MODE_0;
		break;
	}

	return mode;
}


//-------------------------------------------------
//  port_mode -
//-------------------------------------------------

inline int i8255_device::port_mode(int port)
{
	int mode = 0;

	switch (port)
	{
	case PORT_A: mode = m_control & CONTROL_PORT_A_INPUT ? MODE_INPUT : MODE_OUTPUT; break;
	case PORT_B: mode = m_control & CONTROL_PORT_B_INPUT ? MODE_INPUT : MODE_OUTPUT; break;
	}

	return mode;
}


//-------------------------------------------------
//  port_c_lower_mode -
//-------------------------------------------------

inline int i8255_device::port_c_lower_mode()
{
	return m_control & CONTROL_PORT_C_LOWER_INPUT ? MODE_INPUT : MODE_OUTPUT;
}


//-------------------------------------------------
//  port_c_upper_mode -
//-------------------------------------------------

inline int i8255_device::port_c_upper_mode()
{
	return m_control & CONTROL_PORT_C_UPPER_INPUT ? MODE_INPUT : MODE_OUTPUT;
}



//**************************************************************************
//  LIVE DEVICE
//**************************************************************************

//-------------------------------------------------
//  i8255_device - constructor
//-------------------------------------------------

i8255_device::i8255_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock)
    : device_t(mconfig, I8255, "I8255", tag, owner, clock)
{
	m_intr[PORT_A] = m_intr[PORT_B] = 0;
}


//-------------------------------------------------
//  device_config_complete - perform any
//  operations now that the configuration is
//  complete
//-------------------------------------------------

void i8255_device::device_config_complete()
{
	// inherit a copy of the static data
	const i8255_interface *intf = reinterpret_cast<const i8255_interface *>(static_config());
	if (intf != NULL)
		*static_cast<i8255_interface *>(this) = *intf;

	// or initialize to defaults if none provided
	else
	{
		memset(&m_in_pa_cb, 0, sizeof(m_in_pa_cb));
		memset(&m_out_pa_cb, 0, sizeof(m_out_pa_cb));
		memset(&m_in_pb_cb, 0, sizeof(m_in_pb_cb));
		memset(&m_out_pb_cb, 0, sizeof(m_out_pb_cb));
		memset(&m_in_pc_cb, 0, sizeof(m_in_pc_cb));
		memset(&m_out_pc_cb, 0, sizeof(m_out_pc_cb));
	}
}


//-------------------------------------------------
//  device_start - device-specific startup
//-------------------------------------------------

void i8255_device::device_start()
{
	// resolve callbacks
	m_in_port_func[PORT_A].resolve(m_in_pa_cb, *this);
	m_out_port_func[PORT_A].resolve(m_out_pa_cb, *this);
	m_in_port_func[PORT_B].resolve(m_in_pb_cb, *this);
	m_out_port_func[PORT_B].resolve(m_out_pb_cb, *this);
	m_in_port_func[PORT_C].resolve(m_in_pc_cb, *this);
	m_out_port_func[PORT_C].resolve(m_out_pc_cb, *this);

	// register for state saving
	save_item(NAME(m_control));
	save_item(NAME(m_output));
	save_item(NAME(m_input));
	save_item(NAME(m_ibf));
	save_item(NAME(m_obf));
	save_item(NAME(m_inte));
	save_item(NAME(m_inte1));
	save_item(NAME(m_inte2));
	save_item(NAME(m_intr));
}


//-------------------------------------------------
//  device_reset - device-specific reset
//-------------------------------------------------

void i8255_device::device_reset()
{
	set_mode(0x9b);
}


//-------------------------------------------------
//  read_mode0 -
//-------------------------------------------------

UINT8 i8255_device::read_mode0(int port)
{
	UINT8 data = 0;

	if (port_mode(port) == MODE_OUTPUT)
	{
		// read data from output latch
		data = m_output[port];
	}
	else
	{
		// read data from port
		data = m_in_port_func[port](0);
	}

	return data;
}


//-------------------------------------------------
//  read_mode1 -
//-------------------------------------------------

UINT8 i8255_device::read_mode1(int port)
{
	UINT8 data = 0;

	if (port_mode(port) == MODE_OUTPUT)
	{
		// read data from output latch
		data = m_output[port];
	}
	else
	{
		// read data from input latch
		data = m_input[port];

		// clear input buffer full flag
		set_ibf(port, 0);

		// clear interrupt
		set_intr(port, 0);

		// clear input latch
		m_input[port] = 0;
	}

	return data;
}


//-------------------------------------------------
//  read_mode2 -
//-------------------------------------------------

UINT8 i8255_device::read_mode2()
{
	UINT8 data = 0;

	// read data from input latch
	data = m_input[PORT_A];

	// clear input buffer full flag
	set_ibf(PORT_A, 0);

	// clear interrupt
	set_intr(PORT_A, 0);

	// clear input latch
	m_input[PORT_A] = 0;

	return data;
}


//-------------------------------------------------
//  read_pc -
//-------------------------------------------------

UINT8 i8255_device::read_pc()
{
	UINT8 data = 0;
	UINT8 mask = 0;

	// PC upper
	switch (group_mode(GROUP_A))
	{
	case MODE_0:
		if (port_c_upper_mode() == MODE_OUTPUT)
		{
			// read data from output latch
			data |= m_output[PORT_C] & 0xf0;
		}
		else
		{
			// read data from port
			mask |= 0xf0;
		}
		break;

	case MODE_1:
		data |= m_intr[PORT_A] ? 0x08 : 0x00;

		if (port_mode(PORT_A) == MODE_OUTPUT)
		{
			data |= m_obf[PORT_A] ? 0x80 : 0x00;
			data |= m_inte[PORT_A] ? 0x40 : 0x00;
			mask |= 0x30;
		}
		else
		{
			data |= m_ibf[PORT_A] ? 0x20 : 0x00;
			data |= m_inte[PORT_A] ? 0x10 : 0x00;
			mask |= 0xc0;
		}
		break;

	case MODE_2:
		data |= m_intr[PORT_A] ? 0x08 : 0x00;
		data |= m_inte2 ? 0x10 : 0x00;
		data |= m_ibf[PORT_A] ? 0x20 : 0x00;
		data |= m_inte1 ? 0x40 : 0x00;
		data |= m_obf[PORT_A] ? 0x80 : 0x00;
		break;
	}

	// PC lower
	switch (group_mode(GROUP_B))
	{
	case MODE_0:
		if (port_c_lower_mode() == MODE_OUTPUT)
		{
			// read data from output latch
			data |= m_output[PORT_C] & 0x0f;
		}
		else
		{
			// read data from port
			mask |= 0x0f;
		}
		break;

	case MODE_1:
		data |= m_inte[PORT_B] ? 0x04 : 0x00;
		data |= m_intr[PORT_B] ? 0x01 : 0x00;

		if (port_mode(PORT_B) == MODE_OUTPUT)
		{
			data |= m_obf[PORT_B] ? 0x02 : 0x00;
		}
		else
		{
			data |= m_ibf[PORT_B] ? 0x02 : 0x00;
		}
	}

	if (mask)
	{
		// read data from port
		data |= m_in_port_func[PORT_C](0) & mask;
	}

	return data;
}


//-------------------------------------------------
//  write_mode0 -
//-------------------------------------------------

void i8255_device::write_mode0(int port, UINT8 data)
{
	if (port_mode(port) == MODE_OUTPUT)
	{
		// latch output data
		m_output[port] = data;

		// write data to port
		m_out_port_func[port](0, data);
	}
}


//-------------------------------------------------
//  write_mode1 -
//-------------------------------------------------

void i8255_device::write_mode1(int port, UINT8 data)
{
	if (port_mode(port) == MODE_OUTPUT)
	{
		// latch output data
		m_output[port] = data;

		// write data to port
		m_out_port_func[port](0, data);

		// set output buffer full flag
		set_obf(port, 0);

		// clear interrupt
		set_intr(port, 0);
	}
}


//-------------------------------------------------
//  write_mode2 -
//-------------------------------------------------

void i8255_device::write_mode2(UINT8 data)
{
	// latch output data
	m_output[PORT_A] = data;

	// write data to port
	m_out_port_func[PORT_A](0, data);

	// set output buffer full flag
	set_obf(PORT_A, 0);

	// clear interrupt
	set_intr(PORT_A, 0);
}


//-------------------------------------------------
//  write_pc -
//-------------------------------------------------

void i8255_device::write_pc(UINT8 data)
{
	int changed = 0;

	if (group_mode(GROUP_A) == MODE_0)
	{
		// PC upper
		if (port_c_upper_mode() == MODE_OUTPUT)
		{
			m_output[PORT_C] = (data & 0xf0) | (m_output[PORT_C] & 0x0f);
			changed = 1;
		}

		// PC lower
		if (port_c_lower_mode() == MODE_OUTPUT)
		{
			m_output[PORT_C] = (m_output[PORT_C] & 0xf0) | (data & 0x0f);
			changed = 1;
		}
	}

	if (changed)
	{
		output_pc();
	}
}


//-------------------------------------------------
//  output_pc -
//-------------------------------------------------

void i8255_device::output_pc()
{
	UINT8 data = 0;
	UINT8 mask = 0;

	// PC upper
	switch (group_mode(GROUP_A))
	{
	case MODE_0:
		if (port_c_upper_mode() == MODE_OUTPUT)
		{
			mask |= 0xf0;
		}
		else
		{
			// TTL inputs float high
			data |= 0xf0;
		}
		break;

	case MODE_1:
		data |= m_intr[PORT_A] ? 0x08 : 0x00;

		if (port_mode(PORT_A) == MODE_OUTPUT)
		{
			data |= m_obf[PORT_A] ? 0x80 : 0x00;
			mask |= 0x30;
		}
		else
		{
			data |= m_ibf[PORT_A] ? 0x20 : 0x00;
			mask |= 0xc0;
		}
		break;

	case MODE_2:
		data |= m_intr[PORT_A] ? 0x08 : 0x00;
		data |= m_ibf[PORT_A] ? 0x20 : 0x00;
		data |= m_obf[PORT_A] ? 0x80 : 0x00;
		break;
	}

	// PC lower
	switch (group_mode(GROUP_B))
	{
	case MODE_0:
		if (port_c_lower_mode() == MODE_OUTPUT)
		{
			mask |= 0x0f;
		}
		else
		{
			// TTL inputs float high
			data |= 0x0f;
		}
		break;

	case MODE_1:
		data |= m_intr[PORT_B] ? 0x01 : 0x00;

		if (port_mode(PORT_B) == MODE_OUTPUT)
		{
			data |= m_obf[PORT_B] ? 0x02 : 0x00;
		}
		else
		{
			data |= m_ibf[PORT_B] ? 0x02 : 0x00;
		}
	}

	data |= m_output[PORT_C] & mask;

	m_out_port_func[PORT_C](0, data);
}


//-------------------------------------------------
//  set_mode -
//-------------------------------------------------

void i8255_device::set_mode(UINT8 data)
{
	m_control = data;

	// group A
	m_output[PORT_A] = 0;
	m_input[PORT_A] = 0;
	m_ibf[PORT_A] = 0;
	m_obf[PORT_A] = 1;
	m_inte[PORT_A] = 0;
	m_inte1 = 0;
	m_inte2 = 0;

	if (port_mode(PORT_A) == MODE_OUTPUT)
	{
		m_out_port_func[PORT_A](0, m_output[PORT_A]);
	}
	else
	{
		// TTL inputs float high
		m_out_port_func[PORT_A](0, 0xff);
	}

	if (LOG)
	{
		logerror("I8255 '%s' Group A Mode: %u\n", tag(), group_mode(GROUP_A));
		logerror("I8255 '%s' Port A Mode: %s\n", tag(), (port_mode(PORT_A) == MODE_OUTPUT) ? "output" : "input");
		logerror("I8255 '%s' Port C Upper Mode: %s\n", tag(), (port_c_upper_mode() == MODE_OUTPUT) ? "output" : "input");
		logerror("I8255 '%s' Group B Mode: %u\n", tag(), group_mode(GROUP_B));
		logerror("I8255 '%s' Port B Mode: %s\n", tag(), (port_mode(PORT_B) == MODE_OUTPUT) ? "output" : "input");
		logerror("I8255 '%s' Port C Lower Mode: %s\n", tag(), (port_c_lower_mode() == MODE_OUTPUT) ? "output" : "input");
	}

	// group B
	m_output[PORT_B] = 0;
	m_input[PORT_B] = 0;
	m_ibf[PORT_B] = 0;
	m_obf[PORT_B] = 1;
	m_inte[PORT_B] = 0;

	if (port_mode(PORT_B) == MODE_OUTPUT)
	{
		m_out_port_func[PORT_B](0, m_output[PORT_B]);
	}
	else
	{
		// TTL inputs float high
		m_out_port_func[PORT_B](0, 0xff);
	}

	m_output[PORT_C] = 0;
	m_input[PORT_C] = 0;

	output_pc();
}


//-------------------------------------------------
//  set_pc_bit -
//-------------------------------------------------

void i8255_device::set_pc_bit(int bit, int state)
{
	// set output latch bit
	m_output[PORT_C] &= ~(1 << bit);
	m_output[PORT_C] |= state << bit;

	switch (group_mode(GROUP_A))
	{
	case MODE_1:
		if (port_mode(PORT_A) == MODE_OUTPUT)
		{
			switch (bit)
			{
			case 3: set_intr(PORT_A, state); break;
			case 6: set_inte(PORT_A, state); break;
			case 7: set_obf(PORT_A, state); break;
			}
		}
		else
		{
			switch (bit)
			{
			case 3: set_intr(PORT_A, state); break;
			case 4: set_inte(PORT_A, state); break;
			case 5: set_ibf(PORT_A, state); break;
			}
		}
		break;

	case MODE_2:
		switch (bit)
		{
		case 3: set_intr(PORT_A, state); break;
		case 4: set_inte2(state); break;
		case 5: set_ibf(PORT_A, state); break;
		case 6: set_inte1(state); break;
		case 7: set_obf(PORT_A, state); break;
		}
		break;
	}

	if (group_mode(GROUP_B) == MODE_1)
	{
		switch (bit)
		{
		case 0: set_intr(PORT_B, state); break;
		case 1:
			if (port_mode(PORT_B) == MODE_OUTPUT)
				set_obf(PORT_B, state);
			else
				set_ibf(PORT_B, state);
			break;
		case 2: set_inte(PORT_B, state); break;
		}
	}

	output_pc();
}


//-------------------------------------------------
//  read -
//-------------------------------------------------

READ8_MEMBER( i8255_device::read )
{
	UINT8 data = 0;

	switch (offset & 0x03)
	{
	case PORT_A:
		switch (group_mode(GROUP_A))
		{
		case MODE_0: data = read_mode0(PORT_A); break;
		case MODE_1: data = read_mode1(PORT_A); break;
		case MODE_2: data = read_mode2(); break;
		}
		if (LOG) logerror("I8255 '%s' Port A Read: %02x\n", tag(), data);
		break;

	case PORT_B:
		switch (group_mode(GROUP_B))
		{
		case MODE_0: data = read_mode0(PORT_B); break;
		case MODE_1: data = read_mode1(PORT_B); break;
		}
		if (LOG) logerror("I8255 '%s' Port B Read: %02x\n", tag(), data);
		break;

	case PORT_C:
		data = read_pc();
		if (LOG) logerror("I8255 '%s' Port C Read: %02x\n", tag(), data);
		break;

	case CONTROL:
		data = m_control;
		if (LOG) logerror("I8255 '%s' Mode Control Word Read: %02x\n", tag(), data);
		break;
	}

	return data;
}


//-------------------------------------------------
//  write -
//-------------------------------------------------

WRITE8_MEMBER( i8255_device::write )
{
	switch (offset & 0x03)
	{
	case PORT_A:
		if (LOG) logerror("I8255 '%s' Port A Write: %02x\n", tag(), data);

		switch (group_mode(GROUP_A))
		{
		case MODE_0: write_mode0(PORT_A, data); break;
		case MODE_1: write_mode1(PORT_A, data); break;
		case MODE_2: write_mode2(data); break;
		}
		break;

	case PORT_B:
		if (LOG) logerror("I8255 '%s' Port B Write: %02x\n", tag(), data);

		switch (group_mode(GROUP_B))
		{
		case MODE_0: write_mode0(PORT_B, data); break;
		case MODE_1: write_mode1(PORT_B, data); break;
		}
		break;

	case PORT_C:
		if (LOG) logerror("I8255 '%s' Port C Write: %02x\n", tag(), data);

		write_pc(data);
		break;

	case CONTROL:
		if (data & CONTROL_MODE_SET)
		{
			if (LOG) logerror("I8255 '%s' Mode Control Word: %02x\n", tag(), data);

			set_mode(data);
		}
		else
		{
			int bit = (data >> 1) & 0x07;
			int state = BIT(data, 0);

			if (LOG) logerror("I8255 '%s' %s Port C Bit %u\n", tag(), state ? "Set" : "Reset", bit);

			set_pc_bit(bit, state);
		}
		break;
	}
}


//-------------------------------------------------
//  pa_r -
//-------------------------------------------------

READ8_MEMBER( i8255_device::pa_r )
{
	return pa_r();
}


//-------------------------------------------------
//  pb_r - port A read
//-------------------------------------------------

UINT8 i8255_device::pa_r()
{
	UINT8 data = 0xff;

	if (port_mode(PORT_A) == MODE_OUTPUT)
	{
		data = m_output[PORT_A];
	}

	return data;
}


//-------------------------------------------------
//  pb_r -
//-------------------------------------------------

READ8_MEMBER( i8255_device::pb_r )
{
	return pb_r();
}


//-------------------------------------------------
//  pb_r - port B read
//-------------------------------------------------

UINT8 i8255_device::pb_r()
{
	UINT8 data = 0xff;

	if (port_mode(PORT_B) == MODE_OUTPUT)
	{
		data = m_output[PORT_B];
	}

	return data;
}


//-------------------------------------------------
//  pc2_w -
//-------------------------------------------------

WRITE_LINE_MEMBER( i8255_device::pc2_w )
{
	if (group_mode(GROUP_B) == 1)
	{
		if (port_mode(PORT_B) == MODE_OUTPUT)
		{
			// port B acknowledge
			if (!m_obf[PORT_B] && !state)
			{
				if (LOG) logerror("I8255 '%s' Port B Acknowledge\n", tag());

				// clear output buffer flag
				set_obf(PORT_B, 1);
			}
		}
		else
		{
			// port B strobe
			if (!m_ibf[PORT_B] && !state)
			{
				if (LOG) logerror("I8255 '%s' Port B Strobe\n", tag());

				// read port into latch
				m_input[PORT_B] = m_in_port_func[PORT_B](0);

				// set input buffer flag
				set_ibf(PORT_B, 1);
			}
		}
	}
}


//-------------------------------------------------
//  pc4_w -
//-------------------------------------------------

WRITE_LINE_MEMBER( i8255_device::pc4_w )
{
	if ((group_mode(GROUP_A) == 2) || ((group_mode(GROUP_A) == 1) && (port_mode(PORT_A) == MODE_INPUT)))
	{
		// port A strobe
		if (!m_ibf[PORT_A] && !state)
		{
			if (LOG) logerror("I8255 '%s' Port A Strobe\n", tag());

			// read port into latch
			m_input[PORT_A] = m_in_port_func[PORT_A](0);

			// set input buffer flag
			set_ibf(PORT_A, 1);
		}
	}
}


//-------------------------------------------------
//  pc6_w -
//-------------------------------------------------

WRITE_LINE_MEMBER( i8255_device::pc6_w )
{
	if ((group_mode(GROUP_A) == 2) || ((group_mode(GROUP_A) == 1) && (port_mode(PORT_A) == MODE_OUTPUT)))
	{
		// port A acknowledge
		if (!m_obf[PORT_A] && !state)
		{
			if (LOG) logerror("I8255 '%s' Port A Acknowledge\n", tag());

			// clear output buffer flag
			set_obf(PORT_A, 1);
		}
	}
}
